﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Test.Expressions;
using System.Text.RegularExpressions;
using System.Reflection;

namespace Test
{
	public class TSQLTranslator : ExpressionVisitor
	{
		protected StringBuilder sb;
		private Stack<string> indent_stack;
		private Expression root;
		Dictionary<FieldInfo, string> field_map;
		protected TSQLTranslator() {
			this.sb = new StringBuilder();
			this.indent_stack = new Stack<string>();
		}

		public TSQLTranslator(SubqueryExpression root, Dictionary<FieldInfo, string> field_map = null)
			: this() {
			this.root = root;
			this.field_map = field_map;
		}

		public virtual string Translate() {
			this.Visit(this.root);
			return this.sb.ToString();
		}

		public override Expression Visit(Expression node) {

			switch ((DbExpressionType)node.NodeType) {
				case DbExpressionType.Exists:
					this.Write(" EXISTS ");
					return this.VisitSubquery(((ExistsExpression)node).SubqueryExpression);
				case DbExpressionType.Join:
					//return this.VisitJoin(((JoinExpression)node));											
					this.Write(" JOIN ");
					this.Visit(((JoinExpression)node).Second);
					this.WriteLine();
					this.Write("ON ");
					this.Visit(((JoinExpression)node).Condition);
					return node;
				case DbExpressionType.CrossApply:
					if (((CrossApplyExpression)node).Second != null) {
						this.Write("CROSS APPLY ");
						this.Visit(((CrossApplyExpression)node).Second);
					}
					return node;
				//return this.VisitSelectMany((CrossApplyExpression)node);
				case DbExpressionType.Select:
				case DbExpressionType.Subquery:
				case DbExpressionType.Min:
				case DbExpressionType.Max:
				case DbExpressionType.Sum:
				case DbExpressionType.Average:
				case DbExpressionType.Count:
					return this.VisitSubquery(node);
				default:
					return base.Visit(node);//default Visitor behavior for ExpressionType enum
			}
		}

		/// <summary>
		/// sub-query which can be recursively contained in parens, and may or may not be aliased.
		/// </summary>
		/// <param name="node"></param>
		protected virtual Expression VisitSubquery(Expression node) {
			bool isAliasNodeType = false;
			if (node != root) {
				this.PushIndent("\t");
				this.WriteLine("(");
			}

			switch ((DbExpressionType)node.NodeType) {
				case DbExpressionType.Select:
					this.WriteSubquery((SubqueryExpression)node);
					isAliasNodeType = true;
					break;
				case DbExpressionType.Subquery:
				case DbExpressionType.Min:
				case DbExpressionType.Max:
				case DbExpressionType.Sum:
				case DbExpressionType.Average:
				case DbExpressionType.Count:
					this.WriteSubquery((SubqueryExpression)node);
					isAliasNodeType = false;
					break;
			}
			if (node != root) {
				this.PopIndent();
				this.WriteLine();
				this.Write(")");
				if (isAliasNodeType) {
					this.Write(" [");
					this.Write(((dynamic)node).Alias);
					this.WriteLine("] ");
				}
			}
			return node;
		}


		protected virtual void WriteSubquery(SubqueryExpression node) {
			this.Write("SELECT ");

			if (node.IsDistinct) {
				this.Write(" DISTINCT ");
			}
			if (node.Take != null) {
				this.Write("TOP (");
				this.Visit(node.Take);
				this.Write(") ");
			}
			this.WriteColumns(node.Columns.ToArray<ColumnExpression>());
			this.WriteLine();
			this.Write("FROM ");
			this.Visit(node.From);
			foreach (Expression appended in node.Appended) {
				this.WriteLine();
				this.Visit(appended);
			}
			if (node.Where != null) {
				this.WriteLine();
				this.Write("WHERE ");
				this.Visit(node.Where);//Predicate
			}
			//Not implemented: GROUP BY, ORDER BY						
		}


		protected virtual Expression VisitSelectMany(CrossApplyExpression node) {
			//FROM was written by VisitSubquery			
			//this.Visit(node.First);			
			this.Write("CROSS APPLY");
			//this.PushIndent("\t");
			this.Visit(node.Second);//this would be a select subquery	
			//this.PopIndent();
			//todo: handle case of 3rd argument
			return node;
		}


		protected virtual Expression VisitJoin(JoinExpression node) {
			//this.Visit(node.First);
			this.WriteLine();
			switch ((DbExpressionType)node.NodeType) {
				case DbExpressionType.Join:
					this.Write(" JOIN ");
					break;
				case DbExpressionType.LeftJoin:
					this.Write("LEFT JOIN ");
					break;
			}
			this.Visit(node.Second);
			this.WriteLine();
			this.Write("ON ");
			this.Visit(node.Condition);
			return node;
		}



		protected override Expression VisitMember(MemberExpression m) {
			if (m.Expression.NodeType == ExpressionType.Parameter
					&& m.Member.MemberType == MemberTypes.Field) {
				string alias;
				FieldInfo f = ((FieldInfo)m.Member);
				if (this.field_map != null && this.field_map.ContainsKey(f)) {
					alias = this.field_map[f];
					this.Write(((ParameterExpression)m.Expression).Name + ".");
					this.Write("[{0}]", alias);
				}
				else {
					this.Write(((ParameterExpression)m.Expression).Name + ".");
					this.Write("[{0}]", m.Member.Name);
				}				
			}
			return m;
		}

		protected override Expression VisitConstant(ConstantExpression node) {

			switch (Type.GetTypeCode(node.Value.GetType())) {
				case TypeCode.Boolean:
					this.Write("{0}", ((bool)node.Value) ? 1 : 0);
					break;
				case TypeCode.String:
				case TypeCode.DateTime:
					this.Write("'{0}'", node.Value);
					break;
				case TypeCode.Object://assumption: IQueryable or IEnumerable
					this.Write(this.GetTableName(TypeHelper.GetElementType(node.Type)));
					break;
				default:
					this.Write("{0}", node.Value);
					break;
			}
			return node;
		}




		protected override Expression VisitUnary(UnaryExpression node) {
			//TODO: if !
			this.Write("NOT (");
			base.VisitUnary(node);
			this.Write(")");
			return node;
		}

		protected override Expression VisitBinary(BinaryExpression b) {
			string op = this.GetOperator(b);
			Expression left = b.Left;
			Expression right = b.Right;

			this.Write("(");
			switch (b.NodeType) {
				case ExpressionType.And:
				case ExpressionType.AndAlso:
				case ExpressionType.Or:
				case ExpressionType.OrElse:
					if (left.Type.IsBoolean()) {
						this.Visit(left);
						this.Write(" ");
						this.Write(op);
						this.Write(" ");
						this.Visit(right);
					}
					else {
						this.Visit(left);
						this.Write(" ");
						this.Write(op);
						this.Write(" ");
						this.Visit(right);
					}
					break;
				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
					if (right.NodeType == ExpressionType.Constant) {
						ConstantExpression ce = (ConstantExpression)right;
						if (ce.Value == null) {
							this.Visit(left);
							this.Write(" IS NULL");
							break;
						}
					}
					else if (left.NodeType == ExpressionType.Constant) {
						ConstantExpression ce = (ConstantExpression)left;
						if (ce.Value == null) {
							this.Visit(right);
							this.Write(" IS NULL");
							break;
						}
					}
					goto case ExpressionType.LessThan;
				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
				case ExpressionType.Add:
				case ExpressionType.AddChecked:
				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
				case ExpressionType.Divide:
				case ExpressionType.Modulo:
				case ExpressionType.ExclusiveOr:
					this.Visit(left);
					this.Write(" {0} ", op);
					this.Visit(right);
					break;
				default:
					throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
			}
			this.Write(")");

			return b;
		}

		protected virtual string GetTableName(Type objectType) {
			int lastIndex = objectType.FullName.LastIndexOf(".");
			return "[dbo].[" + objectType.FullName.Substring(lastIndex + 1) + "]";
		}

		protected void PushIndent(string indent) {
			this.indent_stack.Push(indent);
		}
		protected void PopIndent() {
			this.indent_stack.Pop();
		}
		protected void Write(string format, params object[] args) {
			this.sb.AppendFormat(format, args);
		}
		protected void WriteLine(string format = "", params object[] args) {

			this.Write(format + "\r\n" + this.CurrentIndent(), args);
		}
		protected void WriteColumn(ColumnExpression c, int i = 0) {
			string prefix = i > 0 ? ", " : "";
			string TSQL;

			if (c.OwnerAlias != null) {
				TSQL = string.Format("{0}.[{1}]", c.OwnerAlias, c.Name);
			}
			else {
				TSQL = string.Format("[{0}]", c.Name);
			}

			if (c.Function != null) {
				TSQL = prefix + string.Format("{0}({1})", this.GetAggregateName(c.Function.Name), TSQL);
			}
			else {
				TSQL = prefix + TSQL;
			}
			if (c.Alias != null) {
				TSQL = TSQL + string.Format(" [{0}]", c.Alias);
			}
			this.Write(TSQL);
		}
		protected void WriteColumns(ColumnExpression[] columns) {
			for (int i = 0, n = columns.Length; i < n; i++) {
				this.WriteColumn(columns[i], i);
			}
		}
		protected string CurrentIndent() {
			string indent_sum = "";
			foreach (var indent in indent_stack) {
				indent_sum += indent;
			}
			return indent_sum;
		}
	}
}
